﻿using PenguinForce.Collisions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Content;

namespace PenguinForce.Entities
{
    public class PolarBear : Entity, ICollisionEntity
    {
        #region Enumerations

        /// <summary>
        /// TankAiState is used to keep track of what the tank is currently doing.
        /// </summary>
        private enum AIState
        {
            // chasing the cat
            Chasing,

            // the tank has gotten close enough that the cat that it can stop chasing it
            Caught,

            /// <summary>
            /// Stop and chew your food.
            /// </summary>
            Eating,

            // the tank can't "see" the cat, and is wandering around.
            Wander
        }

        #endregion

        #region Constants

        // how fast can the tank move?
        const float MAX_SPEED = 2.5f;

        // how fast can he turn?
        const float TURN_SPEED = 0.10f;

        // this value controls the distance at which the tank will start to chase the
        // cat.
        const float CHASE_DISTANCE = 100.0f;

        // TankCaughtDistance controls the distance at which the tank will stop because
        // he has "caught" the cat.
        const float CAUGHT_DISTANCE = 60.0f;

        // this constant is used to avoid hysteresis, which is common in ai programming.
        // see the doc for more details.
        const float HYSTERESIS = 15.0f;

        private const double EATING_TIME = 2000.0; // 2 seconds to eat your food

        #endregion

        #region Variables

        private Random _random = new Random();
        private Vector2 _startPosition;
        private AIState _state;
        private float _orientation;
        private Vector2 _wanderDirection;
        private double _caughtStartTime;

        private List<Texture2D> _animation;

        #endregion

        #region Constructors

        public PolarBear(ContentManager content, Vector2 startPosition)
            : base(null)
        {
            this._startPosition = startPosition;
            this._location = this._startPosition;
            this._state = AIState.Wander;
            this._animaltype = EntityType.PolarBear;

            LoadContent(content);
        }

        #endregion

        #region Properties

        public BoundingSphere Bounds
        {
            get
            {
                return new BoundingSphere(new Vector3(this._location, 0), 32.0f);
            }
        }

        #endregion

        #region Methods

        private void LoadContent(ContentManager content)
        {
            _animation = new List<Texture2D>();
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top00"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top01"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top02"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top03"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top04"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top05"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top06"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top07"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top08"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top09"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top10"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top11"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top12"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top13"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top14"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top15"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top16"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top17"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top18"));
            _animation.Add(content.Load<Texture2D>("PolarBear/polarbear-top19"));
            base.InitializeTexture(_animation[0]);
        }

        public override void Update(GameTime gameTime)
        {
            UpdateAnimation(gameTime);

            if (_state == AIState.Eating)
            {
                // Pause for a moment and finish your meal.
                if ((gameTime.TotalGameTime.TotalMilliseconds - _caughtStartTime) >= EATING_TIME)
                {
                    _state = AIState.Wander;
                }
                else
                {
                    return;
                }
            }

            // However, the tank's behavior is more complicated than the mouse's, and so
            // the decision making process is a little different. 

            // First we have to use the current state to decide what the thresholds are
            // for changing state, as described in the doc.

            float chaseThreshold = CHASE_DISTANCE;
            float caughtThreshold = CAUGHT_DISTANCE;
            // if the tank is idle, he prefers to stay idle. we do this by making the
            // chase distance smaller, so the tank will be less likely to begin chasing
            // the cat.
            if (_state == AIState.Wander)
            {
                chaseThreshold -= HYSTERESIS / 2;
            }
            // similarly, if the tank is active, he prefers to stay active. we
            // accomplish this by increasing the range of values that will cause the
            // tank to go into the active state.
            else if (_state == AIState.Chasing)
            {
                chaseThreshold += HYSTERESIS / 2;
                caughtThreshold -= HYSTERESIS / 2;
            }
            // the same logic is applied to the finished state.
            else if (_state == AIState.Caught)
            {
                caughtThreshold += HYSTERESIS / 2;
            }

            // Second, now that we know what the thresholds are, we compare the tank's 
            // distance from the cat against the thresholds to decide what the tank's
            // current state is.


            Penguin closestPenguin = CollisionManager.Instance.FindClosestPenguin(_location);
            float distanceFromPenguin = float.MaxValue;
            if (closestPenguin != null)
            {
                distanceFromPenguin = Vector2.Distance(_location, closestPenguin.Location);
            }
            if (distanceFromPenguin > chaseThreshold)
            {
                // just like the mouse, if the tank is far away from the cat, it should
                // idle.
                _state = AIState.Wander;
            }
            else if (distanceFromPenguin > caughtThreshold)
            {
                _state = AIState.Chasing;
            }
            else
            {
                _state = AIState.Caught;
            }

            // Third, once we know what state we're in, act on that state.
            float currentSpeed = 0.0f;
            switch (_state)
            {
                case AIState.Chasing:
                    // the tank wants to chase the cat, so it will just use the TurnToFace
                    // function to turn towards the cat's position. Then, when the tank
                    // moves forward, he will chase the cat.
                    _orientation = TurnToFace(_location, closestPenguin.Location, _orientation, TURN_SPEED);
                    currentSpeed = 1.0f * MAX_SPEED;
                    break;
                case AIState.Wander:
                    // wander works just like the mouse's.
                    Wander(_location, ref _wanderDirection, ref _orientation, TURN_SPEED);
                    currentSpeed = 0.5f * MAX_SPEED;
                    break;
                case AIState.Caught:
                    // this part is different from the mouse. if the tank catches the cat, 
                    // it should stop. otherwise it will run right by, then spin around and
                    // try to catch it all over again. The end result is that it will kind
                    // of "run laps" around the cat, which looks funny, but is not what
                    // we're after.
                    currentSpeed = 0.0f * MAX_SPEED;
                    CollisionManager.Instance.Penguins.Kill(closestPenguin);
                    _caughtStartTime = gameTime.TotalGameTime.TotalMilliseconds;
                    _state = AIState.Eating;
                    break;
            }

            // this calculation is also just like the mouse's: we construct a heading
            // vector based on the tank's orientation, and then make the tank move along
            // that heading.
            _direction = new Vector2((float)Math.Cos(_orientation), (float)Math.Sin(_orientation));
            _location += _direction * currentSpeed * CollisionManager.Instance.GetCurrentLevel().GetFriction(_location);
            if (CollisionManager.Instance.ContainsBarrier(this))
            {
                _location -= _direction * currentSpeed * CollisionManager.Instance.GetCurrentLevel().GetFriction(_location);
            }
        }

        private void UpdateAnimation(GameTime gameTime)
        {
            base.InitializeTexture(this._animation[(int)(gameTime.TotalGameTime.TotalMilliseconds / 75) % this._animation.Count]);
        }

        /// <summary>
        /// Wander contains functionality that is shared between both the mouse and the
        /// tank, and does just what its name implies: makes them wander around the
        /// screen. The specifics of the function are described in more detail in the
        /// accompanying doc.
        /// </summary>
        /// <param name="position">the position of the character that is wandering
        /// </param>
        /// <param name="wanderDirection">the direction that the character is currently
        /// wandering. this parameter is passed by reference because it is an input and
        /// output parameter: Wander accepts it as input, and will update it as well.
        /// </param>
        /// <param name="orientation">the character's orientation. this parameter is
        /// also passed by reference and is an input/output parameter.</param>
        /// <param name="turnSpeed">the character's maximum turning speed.</param>
        private void Wander(Vector2 position, ref Vector2 wanderDirection, ref float orientation, float turnSpeed)
        {
            // The wander effect is accomplished by having the character aim in a random
            // direction. Every frame, this random direction is slightly modified.
            // Finally, to keep the characters on the center of the screen, we have them
            // turn to face the screen center. The further they are from the screen
            // center, the more they will aim back towards it.

            // the first step of the wander behavior is to use the random number
            // generator to offset the current wanderDirection by some random amount.
            // .25 is a bit of a magic number, but it controls how erratic the wander
            // behavior is. Larger numbers will make the characters "wobble" more,
            // smaller numbers will make them more stable. we want just enough
            // wobbliness to be interesting without looking odd.
            wanderDirection.X += MathHelper.Lerp(-.25f, .25f, (float)_random.NextDouble());
            wanderDirection.Y += MathHelper.Lerp(-.25f, .25f, (float)_random.NextDouble());

            // we'll renormalize the wander direction, ...
            if (wanderDirection != Vector2.Zero)
            {
                wanderDirection.Normalize();
            }
            // ... and then turn to face in the wander direction. We don't turn at the
            // maximum turning speed, but at 15% of it. Again, this is a bit of a magic
            // number: it works well for this sample, but feel free to tweak it.
            orientation = TurnToFace(position, position + wanderDirection, orientation, .15f * turnSpeed);


            // next, we'll turn the characters back towards the start position, to
            // prevent them from getting stuck on the edges of the screen.

            // Here we are creating a curve that we can apply to the turnSpeed. This
            // curve will make it so that if we are close to the center of the screen,
            // we won't turn very much. However, the further we are from the screen
            // center, the more we turn. At most, we will turn at 30% of our maximum
            // turn speed. This too is a "magic number" which works well for the sample.
            // Feel free to play around with this one as well: smaller values will make
            // the characters explore further away from the center, but they may get
            // stuck on the walls. Larger numbers will hold the characters to center of
            // the screen. If the number is too large, the characters may end up
            // "orbiting" the center.
            float distanceFromScreenCenter = Vector2.Distance(this._startPosition, position);
            float MaxDistanceFromStartPosition = Math.Min(this._startPosition.Y, this._startPosition.X);

            float normalizedDistance = distanceFromScreenCenter / MaxDistanceFromStartPosition;

            float turnToCenterSpeed = 0.3f * normalizedDistance * normalizedDistance * turnSpeed;

            // once we've calculated how much we want to turn towards the center, we can
            // use the TurnToFace function to actually do the work.
            orientation = TurnToFace(position, this._startPosition, orientation, turnToCenterSpeed);
        }

        /// <summary>
        /// Calculates the angle that an object should face, given its position, its
        /// target's position, its current angle, and its maximum turning speed.
        /// </summary>
        private static float TurnToFace(Vector2 position, Vector2 faceThis, float currentAngle, float turnSpeed)
        {
            // consider this diagram:
            //         B 
            //        /|
            //      /  |
            //    /    | y
            //  / o    |
            // A--------
            //     x
            // 
            // where A is the position of the object, B is the position of the target,
            // and "o" is the angle that the object should be facing in order to 
            // point at the target. we need to know what o is. using trig, we know that
            //      tan(theta)       = opposite / adjacent
            //      tan(o)           = y / x
            // if we take the arctan of both sides of this equation...
            //      arctan( tan(o) ) = arctan( y / x )
            //      o                = arctan( y / x )
            // so, we can use x and y to find o, our "desiredAngle."
            // x and y are just the differences in position between the two objects.
            float x = faceThis.X - position.X;
            float y = faceThis.Y - position.Y;

            // we'll use the Atan2 function. Atan will calculates the arc tangent of 
            // y / x for us, and has the added benefit that it will use the signs of x
            // and y to determine what cartesian quadrant to put the result in.
            // http://msdn2.microsoft.com/en-us/library/system.math.atan2.aspx
            float desiredAngle = (float)Math.Atan2(y, x);

            // so now we know where we WANT to be facing, and where we ARE facing...
            // if we weren't constrained by turnSpeed, this would be easy: we'd just 
            // return desiredAngle.
            // instead, we have to calculate how much we WANT to turn, and then make
            // sure that's not more than turnSpeed.

            // first, figure out how much we want to turn, using WrapAngle to get our
            // result from -Pi to Pi ( -180 degrees to 180 degrees )
            float difference = WrapAngle(desiredAngle - currentAngle);

            // clamp that between -turnSpeed and turnSpeed.
            difference = MathHelper.Clamp(difference, -turnSpeed, turnSpeed);

            // so, the closest we can get to our target is currentAngle + difference.
            // return that, using WrapAngle again.
            return WrapAngle(currentAngle + difference);
        }

        /// <summary>
        /// Returns the angle expressed in radians between -Pi and Pi.
        /// <param name="radians">the angle to wrap, in radians.</param>
        /// <returns>the input value expressed in radians from -Pi to Pi.</returns>
        /// </summary>
        private static float WrapAngle(float radians)
        {
            while (radians < -MathHelper.Pi)
            {
                radians += MathHelper.TwoPi;
            }
            while (radians > MathHelper.Pi)
            {
                radians -= MathHelper.TwoPi;
            }
            return radians;
        }

        #endregion
    }
}